65.9K
CodeProject 正在变化。 阅读更多。
Home

如何使用 C# 构建基本的 IVR(交互式语音应答)菜单系统,通过 DTMF/语音控制提升您的呼叫中心

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.99/5 (32投票s)

2014年3月19日

CPOL

13分钟阅读

viewsIcon

128821

downloadIcon

4301

本文详细介绍了如何以最简单的方式开发一个基本的 IVR 语音菜单系统,并解释了如何创建盲转和人工语音控制等专业的 VoIP 功能。

引言

在当今的商业世界中,呼叫中心占据的领域比以往任何时候都多。它们的主要任务是服务客户,这通常由销售和技术支持部门完成,例如产品支持、电话营销或市场调研。

良好的呼叫中心必须能够处理大量并发呼叫,因为对于大型公司而言,可能同时有数百个电话打入。因此,应用有效的呼叫管理选项(呼叫排队、呼叫转接、呼叫保持等)已变得必不可少。

每个运行良好的呼叫中心都有一个交互式语音应答 (IVR) 菜单,它能减轻座席的负担,因为它可以帮助客户访问与他们账户详情或可用产品/服务相关的基本服务。通常,呼叫者可以通过 IVR 访问一些简单的操作,或者您可以要求转接到人工座席。为了更好地理解,IVR 是语音菜单系统,可以将客户引导至他们需要的菜单项。它通过触摸音电话键盘输入 (DTMF 信号) 接收客户的响应。除了 DTMF 信令,IVR 系统还可以通过人工语音命令(语音控制)进行控制。在本项目中,我将解释两者的实现。

必备组件

  1. 我使用 C# 构建了 IVR 应用程序,因此您需要一个支持此编程语言的 IDE(集成开发环境),例如 Microsoft Visual Studio。
  2. 您的 PC 上还需要安装 .NET Framework。
  3. 由于我的 IVR 系统基于 VoIP 技术,因此您需要在 IDE 的引用中添加一些 VoIP 组件,以便以最简单的方式定义 IVR 的默认行为。由于我之前已经为其他 VoIP 开发(我创建了一个 IP PBX)使用了 Ozeki VoIP SIP SDK,因此我使用了该 SDK 预先编写的 VoIP 组件。

IDE(集成开发环境)中的设置

创建一个新项目

  1. 打开 Visual Studio,单击“文件”,然后单击“新建项目”。
  2. 选择“Visual C# 控制台应用程序”选项。
  3. 为您的新项目指定一个名称。
  4. 单击“确定”。

将 VoIP 组件添加到您的引用中,以实现并使用 SDK 的 IVR 组件。

  1. 右键单击“引用”。
  2. 选择“添加引用”选项。
  3. 浏览位于 SDK 安装位置的 VoIPSDK.dll 文件。
  4. 选择 .dll 文件,然后单击“确定”按钮。

编写代码

在我的项目中使用了 3 个类:Softphone.cs、CallHandler.cs;Program.cs。让我们一步一步地查看这 3 个类的实现。

实现 Softphone.cs 类

首先,您需要创建一个简单的软电话,它具有与普通电话相同的功能。(这是必要的,因为 IVR 需要能够接收来自其他电话的呼叫。因此,您将通过此软电话来管理传入呼叫。)`Softphone.cs` 类用于介绍如何声明、定义和初始化软电话,如何处理 VoIP SDK 的某些事件以及如何使用其函数。此类的 Program.cs 中将用于创建新的软电话。

首先,添加一些额外的 using 语句(代码示例 1):

using Ozeki.Network.Nat;
using Ozeki.VoIP;
using Ozeki.VoIP.SDK; 

代码示例 1: 添加一些新的 using 语句

现在您需要从 `ISoftPhone` 和 `IPhoneLine` 接口创建软电话和电话线(**代码示例 2**)。

ISoftPhone softphone;   // softphone object  
IPhoneLine phoneLine;   // phoneline object  

代码示例 2: IsoftPhone 和 IPhoneLine 对象

在构造函数中,您还需要使用默认参数初始化此软电话(**代码示例 3**)。您需要将第一个参数表示的端口范围设置为 `minPortRange`,将最大参数设置为 `maxPortRange`。这是端口的区间。第三个参数是监听端口(5060 是 SIP 的端口)。通过订阅 `IncomingCall` 事件,可以持续监控传入呼叫。

softphone = SoftPhoneFactory.CreateSoftPhone(5000, 10000, 5060);
softphone.IncomingCall += softphone_IncomingCall;

代码示例 3: 软电话的初始化

现在您需要通过使用 Register 方法注册您的**SIP 账户**到服务器(**代码示例 4**)。此方法将注册所需的所有值作为参数:`registrationRequired`、`displayName`、`userName`、`authenticationId`、`registerPassword`、`domainHost` 和 `domainPort`。这些参数将用于通过 SIPAccount 类的构造函数创建 SIP 账户。创建账户后,该方法使用 `NatTraversalMethod` 配置网络地址转换 (NAT),以确保传入呼叫能够穿过防火墙。完成这些步骤后,系统就可以使用账户和 `NatConfiguration` 创建电话线 (`CreatePhoneLine` 方法),从而允许呼叫 IVR。最后,您需要注册此电话线。

public void Register(bool registrationRequired, string displayName, string userName, string authenticationId, string registerPassword, string domainHost, int domainPort)  
{  
    try  
    {  
       var account = new SIPAccount(registrationRequired, displayName, userName, authenticationId, registerPassword, domainHost, domainPort);  
       Console.WriteLine("\n Creating SIP account {0}", account);  
       var natConfiguration = new NatConfiguration(NatTraversalMethod.None);  
  
       phoneLine = softphone.CreatePhoneLine(account, natConfiguration);  
       Console.WriteLine("Phoneline created.");  
  
       phoneLine.PhoneLineStateChanged += phoneLine_PhoneLineStateChanged;  
  
       softphone.RegisterPhoneLine(phoneLine);  
    }  
    catch(Exception ex)  
    {  
       Console.WriteLine("Error during SIP registration" + ex.ToString());  
    }  
}  

代码示例 4: SIP 账户注册

实现 CallHandler.cs 类

现在,让我们看看第二个类。`CallHandler.cs` 用于管理传入呼叫。在收到入站呼叫时,呼叫者会通过扬声器听到问候语,并在收听可选择的菜单项后,呼叫者可以通过按键盘上的按钮选择一个菜单项。

为了管理传入呼叫,您需要创建一些对象:`mediaConnector`、`audioHandler`、`phoneCallAudioSender` 和 `greetingMessageTimer`,它们分别来自 `ICall` 接口和 `MediaConnector`、`AudioHandler`、`PhoneCallAudioSender` 和 `Timer` 类(**代码示例 5**)。

ICall call;  
MediaConnector mediaConnector;  
AudioHandler audioHandler;  
PhoneCallAudioSender phoneCallAudioSender;  
Timer greetingMessageTimer; 

代码示例 5: 添加一些新对象

该类的构造函数获取一个 `ICall` 类型参数,并为该类进行基本设置。它设置 `greetingMessageTimer` 的间隔(30 秒),用于在主菜单级别重复问候语,并将 `phoneCallAudioSender` 附加到呼叫(**代码示例 6**)。

public CallHandler(ICall call)  
{  
      greetingMessageTimer = new Timer();  
      greetingMessageTimer.Interval = 30000;  
      greetingMessageTimer.Elapsed += greetingMessageTimer_Elapsed;  
      this.call = call;  
      phoneCallAudioSender = new PhoneCallAudioSender();  
      phoneCallAudioSender.AttachToCall(call);  
      mediaConnector = new MediaConnector();  
} 

代码示例 6: 添加一些新对象

现在来看一下方法,它们是包含一系列语句的代码块。

该类最重要的类之一是 `Start()` 方法。它将由主方法调用。在此方法中,您可以订阅 `CallStateChanged` 和 `DtmfReceived` 事件,然后接受呼叫(**代码示例 7**)。

  • 呼叫状态更改事件是最重要的事件之一。它会告知服务器和客户端呼叫状态的变化。
  • DTMF 接收事件会告知服务器客户端按下的按钮。它表示呼叫者想要进入另一个菜单级别。
public void Start()  
{  
    call.CallStateChanged += call_CallStateChanged;  
    call.DtmfReceived += call_DtmfReceived;  
    call.Accept();  
}  

代码示例 7: 创建 Start() 方法

在 Start 方法中,您使用 `call_DtmfReceived()` 方法订阅了 `DtmfReceived` 事件,并且可以设置当呼叫者按下 DTMF 按钮时发生的情况。在**代码示例 8** 中可以看到,在此示例中,如果呼叫者按下 1,他/她就可以通过 `TextToSpeech` 类的构造函数听到一些产品信息。通过按下 2,呼叫者可以通过调用 `Mp3ToSpeaker` 方法听到一个 mp3 歌曲示例。

void call_DtmfReceived(object sender, VoIPEventArgs<DtmfInfo> e)  
{  
    DisposeCurrentHandler();  
    switch (e.Item.Signal.Signal)  
    {  
         case 0: break;  
         case 1: TextToSpeech("Product XY has been designed for those software developers who especially interested in VoIP developments. If you prefer .NET programming languages, you might be interested in Product XY."); break;  
         case 2: MP3ToSpeaker(); break;  
    }  
}   

代码示例 8: 当呼叫者按下 DTMF 按钮时发生的情况

1.) TextToSpeech 方法

使用 `TextToSpeech` 方法,您可以添加您想播放给呼叫者的文本消息。它将由 `TextToSpeech` 引擎朗读。只需创建一个 `TextToSpeech` 对象,通过 Media Connector 将其连接到 `phoneCallAudioSender`,然后调用 `AddAndStartText` 方法(**代码示例 9**)。

private void TextToSpeech(string text)  
{  
    var tts = new TextToSpeech();  
    audioHandler = tts;  
  
    mediaConnector.Connect(audioHandler, phoneCallAudioSender);  
    tts.AddAndStartText(text);  
}   

代码示例 9: TextToSpeech 方法

2.) MP3ToSpeaker 方法

使用 `MP3ToSpeaker` 方法的函数,IVR 可以轻松地将 MP3 文件播放给呼叫者(例如,问候语)。您只需要创建一个带有文件路径参数的 `MP3StreamPlayback` 对象,通过 `mediaConnector` 将其连接到 `PhoneCallAudioSender`,然后开始流式传输(`StartStreaming()` 方法)(**代码示例 10**)。

private void MP3ToSpeaker()  
{  
    var mp3Player = new MP3StreamPlayback("../../test.mp3");  
    audioHandler = mp3Player;  
  
    mediaConnector.Connect(mp3Player, phoneCallAudioSender);  
    mp3Player.StartStreaming();  
} 

代码示例 10: MP3ToSpeaker 方法

实现 Program.cs 类

您已经到了需要实现的最后一个类。Program.cs 介绍了软电话和 callHandler 对象的使用,并处理来自呼叫者的控制台和 DTMF 事件。

首先,在 `Main` 部分,您需要创建软电话对象,以便访问 Softphone 类中创建的方法。(然后将有一些关于代码的说明以及 sipAccountInitialization 方法的调用。)您可以在此部分添加您的 SIP 账户值。现在您需要订阅 IncomingCall 事件,以便管理传入呼叫(**代码示例 11**)。

static void Main(string[] args)  
{  
      callHandlers = new List<CallHandler>();  
      var softphone = new Softphone();  
  
      Console.WriteLine("/* Program usage description */");  
  
      sipAccountInitialization(softphone);  
  
      softphone.IncomigCall += softphone_IncomigCall;  
      Console.ReadLine();  
}  

代码示例 11: Main() 方法

由于 `CallHandler` 列表,此 IVR 能够管理多个呼叫。当有呼叫进来时,系统会自动接受该呼叫,然后该呼叫将被添加到列表中。每个后续的传入呼叫都将被添加到列表中。列表将一直包含这些呼叫,直到它们处于活动状态。当呼叫结束时,它将从 `CallHandler` 列表中移除。

`sipAccountInitialization()` 方法负责从用户获取 SIP 账户组件。正如您上面看到的,`SIPAccount` 构造函数需要以下值:

  • 认证 ID
  • 用户名(默认为认证 ID)
  • 显示名称(默认为认证 ID)
  • 密码
  • 域主机(默认为本地主机)
  • 域端口(默认为 5060)

因此,您需要为程序添加这些值,它将使用它们调用 Softphone 类的 Register 方法(**代码示例 12**)。

private static void sipAccountInitialization(Softphone softphone)  
{  
     Console.WriteLine("Please setup your SIP account!\n");  
     Console.WriteLine("Please set your authentication ID: ");  
     var authenticationId = Read("authenticationId", true);  
  
     Console.WriteLine("Please set your user name (default:" +authenticationId +"): ");  
     var userName = Read("userName", false);  
     if (string.IsNullOrEmpty(userName))  
             userName = authenticationId;  
  
     Console.WriteLine("Please set your name to be displayed (default: " +authenticationId +"): ");  
     var displayName = Read("displayName", false);  
     if (string.IsNullOrEmpty(displayName))  
             displayName = authenticationId;  
  
     Console.WriteLine("Please set your registration password: ");  
     var registrationPassword = Read("registrationPassword", true);  
  
     Console.WriteLine("Please set the domain name (default: your local host): ");  
     var domainHost = Read("domainHost", false);  
     if (string.IsNullOrEmpty(domainHost))  
             domainHost = NetworkAddressHelper.GetLocalIP().ToString();  
     Console.WriteLine(domainHost);  
  
     Console.WriteLine("Please set the port number (default: 5060): ");  
     int domainPort;  
     string port = Read("domainPort", false);  
     if (string.IsNullOrEmpty(port))  
     {  
           domainPort = 5060;  
     }  
     else  
     {  
           domainPort = Int32.Parse(port);  
     }  
     Console.WriteLine("\nCreating SIP account and trying to register...\n");  
     softphone.Register(true, displayName, userName, authenticationId, registrationPassword, domainHost, domainPort);  
}   

代码示例 12: sipAccountInitialization() 方法

`Read()` 函数有 2 个参数:`inputname` 和 `readWhileEmpty`。如果 `readWhileEmpty` 为 false,则此组件有一个默认值(例如,域端口为 5060)。在这种情况下,输入可以为空或 null。否则,您需要为系统添加一个输入,函数将把输入返回到 `Main` 方法(**代码示例 13**)。

private static string Read(string inputName, bool readWhileEmpty)  
{  
     while (true)  
     {  
          string input = Console.ReadLine();  
  
          if (!readWhileEmpty)  
          {  
               return input;  
          }  
  
          if (!string.IsNullOrEmpty(input))  
          {  
               return input;  
          }  
  
          Console.WriteLine(inputName +" cannot be empty!");  
          Console.WriteLine(inputName +": ");  
    }  
}   

代码示例 13: Read() 函数

为了在收到传入呼叫时得到通知,您需要为此目的专门设置一个事件。这就是 `IncomingCall` 事件。如果有关于等待接受的呼叫的通知,呼叫必须由 IVR 系统接受。在 `softphone_IncomingCall()` 方法中,有一个 `callHandler` 对象用于管理 `CallHandler` 类的所有方法。如果有传入呼叫,请调用 `CallHandler` 的 `Start()` 方法(**代码示例 14**)。

static void softphone_IncomigCall(object sender, Ozeki.VoIP.VoIPEventArgs<Ozeki.VoIP.IPhoneCall> e)  
{  
     Console.WriteLine("Incoming call!");  
     var callHandler = new CallHandler(e.Item);  
     callHandler.Completed += callHandler_Completed;  
  
     lock (callHandlers)  
           callHandlers.Add(callHandler);  
  
     callHandler.Start();  
}

代码示例 14: softphone_IncomingCall() 方法

盲转的实现

现在是时候看看如何通过盲转功能优化您的 IVR 了。

呼叫转接可以由呼叫中心服务器应用程序自动完成,也可以由人工座席协调。在盲转的情况下,第一种选择是最常见的。盲转意味着呼叫将被转接到一个随机选择的终点,基本上是第一个可用的座席。

如果用户选择此选项,系统将自动将呼叫转接到另一个电话号码。为此,您需要对此基本 IVR 代码进行一些修改。

在 Program 类的开头,创建一个静态字符串变量来存储盲转值,然后用户需要输入一个电话号码,如果他/她想使用盲转功能。如果他/她不想使用此选项,则按“0”(**代码示例 15**)。

Console.WriteLine("Please set the number for blind transferring! If you don't want to use this function of the IVR, press 0!");
blindTransfer = Read("blindTransfer", true); 

代码示例 15: 创建静态字符串变量

之后,您需要将此值传递给 `CallHandler` 类。您可以通过在代码的 `softphone_IncomingCall` 部分调用一个新的(例如 `blindTransferNumber()`)方法来实现。通过这种方式,您可以将 `blindTransfer` 值传递给 `CallHandler` 类(**代码示例 16**)。

public void BlindTransferNumber(string blindTransfer)
{
            blindTransferNumber = blindTransfer;
} 

代码示例 16: blindTransferNumber() 方法

您唯一剩下的工作就是在 `call_DtmfReceived()` 方法的 switch 语句中添加一个新的 case。如果用户按下 3 并且他/她提供的数字在程序类中为零,则在控制台写消息表明他/她无法使用此 IVR 功能。否则,调用 `blindtransfer` 方法并将其 blindtransfer number 值作为参数传递给它(**代码示例 17**)。

case 3:
                    {
                        if (blindTransferNumber == "0")
                        {
                            TextToSpeech("You did not add any number for blind transferring!");
                            break;
                        }
                        else
                        {
                            call.BlindTransfer(blindTransferNumber);
                            break;
                        }
                    } 

代码示例 17: 在 call_DtmfReceived() 方法的 switch 语句中添加一个新 case

语音控制 IVR 的实现

为了使 IVR 菜单系统更有效,您可以通过语音控制来方便呼叫者。这意味着他们可以使用人工语音命令在菜单项之间导航,而无需按下任何 DTMF 按钮。

为了实现基于人工语音的控制,您需要修改 `CallHandler` 类,并创建 `SpeechToText` 抽象类的事件和方法。

如上所示,`CallHandler.cs` 用于管理传入呼叫。为了实现语音控制功能,首先需要创建一些新对象(**代码示例 18**)。

  • `PhoneCallAudioReceiver`:用于接收来自呼叫者的音频。
  • `IEnumerable`:用于向系统提供词语选项,以便系统能够识别它们。
  • `SpeechToText`:用于将人工语音转换为文本格式。
PhoneCallAudioReceiver phoneCallAudioReceiver;  
IEnumerable<string> choices;  
SpeechToText stt;

代码示例 18: 在 CallHandler.cs 类中添加一些新对象

**代码示例 19** 显示了 `CallHandler()` 构造函数,您需要在其中设置创建的对象并为其创建实例。`PhoneCallAudioReceiver` 对象应附加到呼叫。此外,您需要使用 `SpeechToText` 类的 `CreateInstance` 方法为 `SpeechToText` 对象创建实例。为了让系统能够识别呼叫者发音的语音命令,您需要向 choices 列表添加一些单词。

public CallHandler(ICall call)  
{  
      greetingMessageTimer = new Timer();  
      greetingMessageTimer.Interval = 30000;  
      greetingMessageTimer.Elapsed += greetingMessageTimer_Elapsed;  
      this.call = call;  
      phoneCallAudioSender = new PhoneCallAudioSender();  
      phoneCallAudioSender.AttachToCall(call);  
      mediaConnector = new MediaConnector();  
      phoneCallAudioReceiver = new PhoneCallAudioReceiver();  
      phoneCallAudioReceiver.AttachToCall(call);  
      choices = new List<string>() { "first", "second"};  
      stt = SpeechToText.CreateInstance(choices);  
}

代码示例 19: CallHandler() 构造函数

**代码示例 20** 中显示了 `Start()` 方法。在此部分,您需要通过 `mediaConnector` 将 `PhoneCallAudioReceiver` 连接到 stt。结果,系统将能够开始识别人工语音。您还需要订阅 `SpeechToText` 类的 `WordHypothesized` 事件。

public void Start()  
{  
     mediaConnector.Connect(phoneCallAudioReceiver, stt);  
     call.CallStateChanged += call_CallStateChanged;  
     stt.WordHypothesized += CallHandler_WordHypothesized;  
     call.Accept();  
}

代码示例 20: Start() 方法

当系统检测到人工语音并且识别出的单词等于 choices 列表中的一个单词时,将启动 `CallHandler_WordHypothesized()` 方法。此方法管理 switch 语句。**如下所示**,如果呼叫者说“first”,则可以通过 `TextToSpeech` 类的构造函数听到简短的产品信息。如果他/她说“second”,则呼叫者可以通过调用 `Mp3ToSpeaker` 方法听到一个 mp3 歌曲示例。

void CallHandler_WordHypothesized(object sender, SpeechDetectionEventArgs e)  
{  
      DisposeCurrentHandler();  
      Console.WriteLine(e.Word.ToString());  
      switch (e.Word.ToString())  
      {  
           case "first": TextToSpeech("Product XY has been designed for those software developers who especially interested in VoIP developments. If you prefer .NET programming languages, you might be interested in Product XY."); break;  
           case "second": MP3ToSpeaker(); break;  
      }  
}

代码示例 21: CallHandler_WordHypothesized() 方法

如何创建多级 IVR

考虑到在当今的商业世界中,更高级的多级 IVR 被广泛使用,我对我的 IVR 解决方案进行了改进。我创建了一个高级菜单系统,可以将呼叫者导航到多个菜单级别。

由于本文演示了如何构建一个基本的 IVR,我认为最好将我的改进作为技巧来呈现。如果您有兴趣开发一个多级 IVR 菜单系统,请研究我的技巧,其中逐步解释了其实现。

如何用 C# 创建多级 IVR(交互式语音应答)菜单系统: https://codeproject.org.cn/Tips/752443/How-to-create-a-multi-level-IVR-Interactive-Voice

摘要

总而言之,呼叫中心如果能够处理大量并发呼叫并拥有先进的呼叫管理功能,那么它就可以非常有效。在我的项目中,我开发了一个基本的 IVR,它可以无需人工干预就能接收和管理传入呼叫。通过构建盲转功能,我的 IVR 可以通过使用 DTMF 信令自动将呼叫者转接到实时座席。当然,您可以通过更多专业功能来扩展您的 IVR(从而改进您的呼叫中心),例如呼叫队列、语音信箱和呼叫录音等。

参考文献

理论背景

下载必要的软件

补充信息

© . All rights reserved.